﻿using System;
using System.ComponentModel;
using System.Runtime.Serialization;

using SharpGL.Media3D.NETDesignSurface.Converters;

namespace SharpGL.Media3D
{
    [TypeConverter(typeof(Vector3DConverter))]
    [DataContract(/*IsReference = true, */Name = "Vector3D", Namespace = "SharpGL")]
    public struct Vector3D
    {
        #region Private Fields

        [DataMember()]
        private float x;

        [DataMember()]
        private float y;

        [DataMember()]
        private float z;

        #endregion Private Fields
        
        #region Constructors
        
        public Vector3D(float x, float y, float z)
        {
            this.x = x;
            this.y = y;
            this.z = z;
        }

        public Vector3D(Vector3D vector)
        {
            this.x = vector.x;
            this.y = vector.y;
            this.z = vector.z;
        }

        #endregion Constructors

        #region Public Methods

        public override int GetHashCode()
        {
            return this.x.GetHashCode() ^
                   this.y.GetHashCode() ^
                   this.z.GetHashCode();
        }

        public override bool Equals(object obj)
        {
            if (obj == null || !(obj is Vector3D))
            {
                return false;
            }

            return Equals(this, (Vector3D)obj);
        }

        public bool Equals(Vector3D vector)
        {
            return Equals(this, vector);
        }

        public static bool Equals(Vector3D vector1, Vector3D vector2)
        {
            return vector1.x.Equals(vector2.x) &&
                   vector1.y.Equals(vector2.y) &&
                   vector1.z.Equals(vector2.z);
        }

        public override string ToString()
        {
            return "(" + this.X + "; " + this.Y + "; " + this.Z + ")";
        }

        public static float AngleBetween(Vector3D vector1, Vector3D vector2)
        {
            if (vector1 == vector2)
            {
                return 0.0f;
            }
            else
            {
                vector1.Normalize();
                vector2.Normalize();
                return ((float)(180.0 / Math.PI * Math.Acos(Vector3D.DotProduct(vector1, vector2))));
            }
        }
        
        public static float DotProduct(Vector3D vector1, Vector3D vector2)
        {
            return vector1.x * vector2.x + vector1.y * vector2.y + vector1.z * vector2.z;
        }

        public static Vector3D CrossProduct(Vector3D vector1, Vector3D vector2)
        {
            return new Vector3D(vector1.y * vector2.z - vector1.z * vector2.y,
                                vector1.z * vector2.x - vector1.x * vector2.z,
                                vector1.x * vector2.y - vector1.y * vector2.x);
        }

        public void Negate()
        {
            this.x *= -1;
            this.y *= -1;
            this.z *= -1;
        }

        public void Normalize()
        {
            float length = this.Length;
            if (length != 0.0f)
            {
                this.x /= length;
                this.y /= length;
                this.z /= length;
            }
        }

        public static Point3D Add(Vector3D vector, Point3D point)
        {
            return new Point3D(vector.x + point.X, vector.y + point.Y, vector.z + point.Z);
        }

        public static Vector3D Add(Vector3D vector1, Vector3D vector2)
        {
            return new Vector3D(vector1.x + vector2.x, vector1.y + vector2.y, vector1.z + vector2.z);
        }

        public static Point3D Subtract(Vector3D vector, Point3D point)
        {
            return new Point3D(vector.x - point.X, vector.y - point.Y, vector.z - point.Z);
        }

        public static Vector3D Subtract(Vector3D vector1, Vector3D vector2)
        {
            return new Vector3D(vector1.x - vector2.x, vector1.y - vector2.y, vector1.z - vector2.z);
        }

        public static Vector3D Multiply(float scalar, Vector3D vector)
        {
            return new Vector3D(vector.x * scalar, vector.y * scalar, vector.z * scalar);
        }

        public static Vector3D Multiply(Vector3D vector, float scalar)
        {
            return new Vector3D(vector.x * scalar, vector.y * scalar, vector.z * scalar);
        }

        public static Vector3D Multiply(Vector3D vector, Matrix matrix)
        {
            if ((Object)matrix == null || matrix.Rows != 4 || matrix.Columns != 4)
                throw new ArgumentException("Dimentions of matrix must be 4x4.");

            float X = vector.X * (float)matrix[0, 0] + vector.Y * (float)matrix[1, 0] + vector.Z * (float)matrix[2, 0];
            float Y = vector.X * (float)matrix[0, 1] + vector.Y * (float)matrix[1, 1] + vector.Z * (float)matrix[2, 1];
            float Z = vector.X * (float)matrix[0, 2] + vector.Y * (float)matrix[1, 2] + vector.Z * (float)matrix[2, 2];

            return new Vector3D(X, Y, Z);
        }

        public static Vector3D Multiply(Matrix matrix, Vector3D vector)
        {
            if ((Object)matrix == null || matrix.Rows != 4 || matrix.Columns != 4)
                throw new ArgumentException("Dimentions of matrix must be 4x4.");

            float X = vector.X * (float)matrix[0, 0] + vector.Y * (float)matrix[0, 1] + vector.Z * (float)matrix[0, 2];
            float Y = vector.X * (float)matrix[1, 0] + vector.Y * (float)matrix[1, 1] + vector.Z * (float)matrix[1, 2];
            float Z = vector.X * (float)matrix[2, 0] + vector.Y * (float)matrix[2, 1] + vector.Z * (float)matrix[2, 2];

            return new Vector3D(X, Y, Z);
        }

        public static Vector3D Divide(Vector3D vector, float scalar)
        {
            float div = 1.0f / scalar;
            return new Vector3D(vector.x * div, vector.y * div, vector.z * div);
        }

        public void Set(float X, float Y, float Z)
        {
            this.X = X;
            this.Y = Y;
            this.Z = Z;
        }

        #endregion Public Methods

        #region Public Operators

        public static Point3D operator +(Vector3D vector, Point3D point)
        {
            return Add(vector, point);
        }

        public static Vector3D operator +(Vector3D vector1, Vector3D vector2)
        {
            return Add(vector1, vector2);
        }

        public static Point3D operator -(Vector3D vector, Point3D point)
        {
            return Subtract(vector, point);
        }

        public static Vector3D operator -(Vector3D vector1, Vector3D vector2)
        {
            return Subtract(vector1, vector2);
        }

        public static Vector3D operator -(Vector3D vector)
        {
            return new Vector3D(-vector.x, -vector.y, -vector.z);
        }

        public static Vector3D operator *(float scalar, Vector3D vector)
        {
            return Multiply(scalar, vector);
        }

        public static Vector3D operator *(Vector3D vector, float scalar)
        {
            return Multiply(vector, scalar);
        }

        public static Vector3D operator *(Vector3D vector, Matrix matrix)
        {
            return Multiply(vector, matrix);
        }

        public static Vector3D operator *(Matrix matrix, Vector3D vector)
        {
            return Multiply(matrix, vector);
        }

        public static Vector3D operator /(Vector3D vector, float scalar)
        {
            return Divide(vector, scalar);
        }

        public static bool operator ==(Vector3D vector1, Vector3D vector2)
        {
            return Equals(vector1, vector2);
        }

        public static bool operator !=(Vector3D vector1, Vector3D vector2)
        {
            return !Equals(vector1, vector2);
        }

        public static explicit operator Point3D(Vector3D vector)
        {
            return new Point3D(vector.x, vector.y, vector.z);
        }

        public static implicit operator float[](Vector3D vector)
        {
            return new float[] { vector.X, vector.Y, vector.Z };
        }

        #endregion Public Operators

        #region Public Properties

        public float X
        {
            get { return this.x; }
            set { this.x = value; }
        }

        public float Y
        {
            get { return this.y; }
            set { this.y = value; }
        }

        public float Z
        {
            get { return this.z; }
            set { this.z = value; }
        }

        public float Length
        {
            get
            {
                return (float)Math.Sqrt(this.x * this.x + this.y * this.y + this.z * this.z);
            }
        }

        [Browsable(false)]
        public float LengthSquared
        {
            get
            {
                return this.x * this.x + this.y * this.y + this.z * this.z;
            }
        }

        #endregion Punlic Properties
    }
}
